#include <cassert>
#include <iostream>
#include <cstdlib>
#include <string>
#include <vector>
#include <map>
#include <fstream>
#include <algorithm>
#include <sstream>
#include <stdexcept>

#include "zip.h"
#include "pugi/pugixml.hpp"

class ConverterXLSXtoXML {
public:
	ConverterXLSXtoXML() {
	}
	~ConverterXLSXtoXML() {
		for (std::ofstream* out : output_files) {
			*out << std::flush;
			delete out;
		}
	}

	ConverterXLSXtoXML(const ConverterXLSXtoXML&) = delete;
	ConverterXLSXtoXML(const ConverterXLSXtoXML&&) = delete;
	ConverterXLSXtoXML& operator =(const ConverterXLSXtoXML&) = delete;

	inline void convert(const std::string& template_, const std::string& exel_file,
			const std::string& output_base_name) {

		shared_strings_xml = readFileFromZipArch(exel_file,
				"xl/sharedStrings.xml");

		sheet1_xml = readFileFromZipArch(exel_file, "xl/worksheets/sheet1.xml");

		collect_strings();
		collect_rows();
		fillSearchTable();

		std::ifstream template_in(template_.c_str(), std::ios::binary);

		if(sheet_data[0].size() <= 1)
		{
			std::string err("no lang found in: ");
			err += exel_file;
			throw std::runtime_error(err);
		}
		num_of_langs = sheet_data[0].size() - 1;

		open_output_file_streams(num_of_langs, output_base_name);

		while (template_in.good() && !template_in.eof()) {
			switch (current_state) {
				case ParserState::read_char: {
					ifNot_CopyToOutput(template_in);
				}
				break;
				case ParserState::count_under: {
					countUnderscores(template_in);
				}
				break;
				case ParserState::read_id: {
					readIdAndCopyToAllOutputValues(template_in);
				}
				break;
			}
		}
	}
private:
	enum class ParserState {
		read_char, count_under, read_id
	};

	ParserState current_state = ParserState::read_char;
	size_t counter_of_underscores = 0u;
	size_t num_of_langs = 0u;
	std::string start_of_marker_id;

	static std::vector<char> readFileFromZipArch(std::string zip_file_name,
			std::string file_name) {
		//Open the ZIP archive
		int err = 0;
		zip *z = zip_open(zip_file_name.c_str(), 0, &err);
		if (!z)
		{
			std::string err("can't open zip file: ");
			err += zip_file_name;
			throw std::runtime_error(err);
		}

		//Search for the file of given name
		struct zip_stat st;
		zip_stat_init(&st);
		if (-1 == zip_stat(z, file_name.c_str(), 0, &st)) {
			std::string err("can't find file: ");
			err += file_name;
			err += "zip file: ";
			err += zip_file_name;
			throw std::runtime_error(err);
		}

		//Alloc memory for its uncompressed contents
		std::vector<char> contents;
		contents.resize(st.size);

		//Read the compressed file
		zip_file *f = zip_fopen(z, file_name.c_str(), 0);
		if (!f) {
			std::string err("can't read compressed file: ");
			err += file_name;
			err += "zip file: ";
			err += zip_file_name;
			throw std::runtime_error(err);
		}
		zip_fread(f, &contents[0], contents.size());

		zip_fclose(f);

		//And close the archive
		zip_close(z);

		return contents;
	}

	inline void collect_strings() {
		pugi::xml_document doc_strings;

		doc_strings.load_buffer_inplace(&shared_strings_xml[0],
				shared_strings_xml.size());

		pugi::xml_node sst_node = doc_strings.first_child();

		std::size_t unique_count = sst_node.attribute("uniqueCount").as_uint();

		strings.reserve(unique_count);

		for (pugi::xml_node node = sst_node.child("si"); node;
				node = node.next_sibling("si")) {
			std::string str = node.child("t").child_value();
			if ("" == str)
			{
				for (pugi::xml_node r = node.child("r"); r; r = r.next_sibling("r"))
				{
					str += r.child("t").child_value();
				}
			}
			strings.push_back(str);
		}
	}

	static size_t countCellsInTableHeader(pugi::xml_node first_row)
	{
		size_t result = 0u;
		for (pugi::xml_node col = first_row.child("c"); col;
				col = col.next_sibling("c")) {
			pugi::xml_node value_node = col.child("v");
			std::string str;
			if (!value_node)
			{
				break;
			}
			++result;
		}
		assert(result > 0u);
		return result;
	}

	inline void collect_rows() {
		pugi::xml_document doc_strings;

		doc_strings.load_buffer_inplace(&sheet1_xml[0],
				sheet1_xml.size());

		pugi::xml_node worksheet = doc_strings.first_child();

		size_t row_count = 2048;
		pugi::xml_node first_row = worksheet.child("sheetData").child("row");
		size_t col_count = countCellsInTableHeader(first_row);
		sheet_data.reserve(row_count);

		for (pugi::xml_node row_node = first_row; row_node;
				row_node = row_node.next_sibling("row")) {

			std::vector<size_t> row;
			row.reserve(col_count);
			for (pugi::xml_node col = row_node.child("c"); col;
					col = col.next_sibling("c")) {
				pugi::xml_node value_node = col.child("v");
				std::string str;
				if (!value_node)
				{
					str = "-1"; // empty cell
				} else
				{
					str = value_node.child_value();
				}

				size_t str_index = (size_t) atoi(str.c_str());
				row.push_back(str_index);
			}
			row.resize(col_count, (size_t)-1);
			sheet_data.push_back(row);
		}
	}

	inline void open_output_file_streams(size_t num_of_langs,
			const std::string& output_file) {

		output_files.reserve(num_of_langs);

		for (size_t i = 1; i <= num_of_langs; ++i) {
			size_t str_index = sheet_data[0].at(i);
			std::string locale_name = strings.at(str_index);
			std::transform(locale_name.begin(), locale_name.end(), locale_name.begin(),
					::tolower);

			std::string full_output_path = output_file + "/" + locale_name + "/strings.xml";
			std::ofstream* out(
					new std::ofstream(full_output_path.c_str(),
							std::ios::trunc | std::ios::binary));
			if (!out || !out->is_open())
			{
				std::string err("can't open file: \"");
				err += full_output_path;
				err += "\"";

				throw std::runtime_error(err);
			}
			output_files.push_back(out);
		}
	}

	inline std::vector<size_t>& findRowBy(const std::string& id) {
		for (std::vector<size_t>& row : sheet_data) {
			size_t str_index = row[0];
			std::string& str_id = strings.at(str_index);
			if (id == str_id) {
				return row;
			}
		}

		std::string err("can't find value by id: ");
		err += id;

		throw std::runtime_error(err.c_str());
	}

	inline void printToAllOutput(char c) {
		for (std::ofstream* out : output_files) {
			out->put(c);
		}
	}

	inline void ifNot_CopyToOutput(std::ifstream& template_in) {
		char c;
		template_in.get(c);
		if (template_in.good())
		{
			if ('_' != c) {
				printToAllOutput(c);
			} else {
				template_in.unget();
				counter_of_underscores = 0u;
				current_state = ParserState::count_under;
			}
		}

	}

	inline void countUnderscores(std::ifstream& template_in) {
		char c;
		template_in.get(c);
		if ('_' != c) {
			template_in.unget();
			while (counter_of_underscores > 0u) {
				printToAllOutput('_');
				--counter_of_underscores;
			}
			current_state = ParserState::read_char;
		} else {
			++counter_of_underscores;
			if (3 == counter_of_underscores) {
				counter_of_underscores = 0u;
				current_state = ParserState::read_id;
			}
		}
	}

	inline void convert_quots_and_ampersands(std::string& str)
	{
		std::string str_out;
		str_out.reserve(str.size() + 16);
		bool change_str = false;
		for(char ch : str)
		{
			if ('\"' == ch)
			{
				change_str = true;
				str_out += "&quot;";
			} else if ('&' == ch)
			{
				change_str = true;
				str_out += "&amp;";
			} else
			{
				str_out.push_back(ch);
			}
		}
		if (change_str)
		{
			str = str_out;
		}
	}

	inline void readIdAndCopyToAllOutputValues(std::ifstream& template_in) {
		char c;
		template_in.get(c);
		if ('\"' == c) // end of id
		{
			template_in.unget();
			std::string id = start_of_marker_id;
			std::vector<size_t>& row = findRowBySpeed(id);
			for (size_t i = 1; i <= num_of_langs; ++i) {
				assert(i < row.size());
				size_t str_index = row[i];
				std::string* str_ptr = nullptr;
				if ((size_t)-1 == str_index)
				{
					// if we have english translate
					if (i > 1u && row[1u] != (size_t)-1)
					{
						// try english
						size_t str_index = row[1u];
						str_ptr = &strings.at(str_index);
					} else
					{
						str_ptr = &empty;
					}
				} else
				{
					str_ptr = &strings.at(str_index);
				}
				size_t stream_index = i - 1;
				assert(stream_index < output_files.size());
				std::ofstream& out = *output_files[stream_index];
				convert_quots_and_ampersands(*str_ptr);
				out << *str_ptr;
			}
			start_of_marker_id.clear();
			current_state = ParserState::read_char;
		} else {
			start_of_marker_id.push_back(c);
		}
	}

	void fillSearchTable()
	{
		for(std::vector<size_t>& row : sheet_data)
		{
			size_t str_index = row[0];
			if ((size_t)-1 != str_index)
			{
				std::string& str_id = strings.at(str_index);
				search_table.insert(std::make_pair(str_id, &row));
			}
		}
	}

	inline std::vector<size_t>& findRowBySpeed(const std::string& id) {
		auto it = search_table.find(id);
		if (it == search_table.end())
		{
			std::string err("can't find value by id: \"");
			err += id;
			err += "\"";
			throw std::runtime_error(err.c_str());
		}
		return *(it->second);
	}

	std::map<std::string, std::vector<size_t>* > search_table;

	std::vector<char> shared_strings_xml;
	std::vector<char> sheet1_xml;

	std::vector<std::string> strings;
	std::vector<std::vector<size_t> > sheet_data;

	std::vector<std::ofstream*> output_files;
	std::string empty;
}; // end class ConverterXLSXtoXML

void echo_usage() {
	std::cout << u8"usage: xlsx-to-xml <template_file> "
	"<exel_file> <output_file_prefix>";
}

int main(int argc, const char* argv[]) {
	{
		if (argc < 3) {
			echo_usage();
			return EXIT_FAILURE;
		}
		std::string template_file = argv[1];
		std::string exel_file = argv[2];
		std::string output_file = argv[3];

		ConverterXLSXtoXML converter;
		try {
			converter.convert(template_file, exel_file, output_file);
		} catch (std::exception& ex) {
			std::cout << ex.what();
			return EXIT_FAILURE;
		}
	}

	return EXIT_SUCCESS;
}

